package mgoasync

import (
	"container/list"
	"context"
	"fmt"
	"time"

	"gitee.com/terender/sfw/rlog"
	"go.mongodb.org/mongo-driver/mongo"
)

// 批量插入后台任务
// 用于频繁生成且生成后不再发生改变的数据批量插入MongoDB，通常用于处理时序型数据(例如日志数据)
// 由于数据生成后不再发生改变，打包时只需要将数据实体直接传入，无需加锁也无需序列化
type InsertTask struct {
	*Task
}

// 构造一个后台插入任务
func NewInsertTask(coll *mongo.Collection, db, col string) *InsertTask {
	return &InsertTask{Task: newTask(coll)}
}

// 任务开始执行
func (task *InsertTask) Begin() error {
	if err := task.begin(); err != nil {
		return err
	}
	go task.insertLoop()
	return nil
}

// 将待插入数据推入队列
func (task *InsertTask) PushData(data interface{}) {
	task.Lock()
	defer task.Unlock()
	task.queue.PushBack(data)
}

// 弹出数据实体链表，确保写锁时间最短
func (task *InsertTask) popQueue() *list.List {
	task.Lock()
	defer task.Unlock()
	q := list.New()
	q.PushBackList(task.queue)
	task.queue.Init()
	return q
}

// insertLoop 批量插入数据库的线程
func (task *InsertTask) insertLoop() {
	defer func() {
		task.setStatus(StatusIdle)
		close(task.closed)
	}()
	name := fmt.Sprintf("%v.%v", task.mgoColl.Database().Name(), task.mgoColl.Name())

	for {
		// 不是退出状态，也没有失败后等待重写的数据，此时需要等待时间间隔
		if task.pendingDatas == nil && !task.isQuiting() {
			select {
			case <-task.ticker.C:
			case <-task.closing:
			}
		}

		// 写入统计数据
		act := &Act{OP: OPUpdate, StartTime: time.Now()}

		// 如果待插入数据为空，则将等待队列中的数据全部取出，放入待插入数据
		// 如果待插入数据不为空，说明上次批量插入数据库操作失败了，此时将待插入数据数据重新写入
		var datas []interface{}
		if task.pendingDatas == nil {
			queue := task.popQueue()
			for e := queue.Front(); e != nil; e = e.Next() {
				datas = append(datas, e.Value)
			}
		} else {
			datas = task.pendingDatas.([]interface{})
			task.pendingDatas = nil
		}
		// 打包完成，记录数据长度和打包时间
		act.PackTime = time.Now()
		act.Count = len(datas)

		// 没有任何待插入数据，此次插入操作直接完成，准备开始下一次
		if len(datas) == 0 {
			act.FinishTime = time.Now()
			// 如果已经处于退出过程中, 则表示全部数据回写完成，可以退出
			if task.isQuiting() {
				rlog.Infof(`异步批量插入 %v.%v 完成，写入了 %v 个文档`, task.mgoColl.Database().Name(), task.mgoColl.Name(), len(datas))
				break
			}
			continue
		}
		addStatistics(name, act)

		// 将数据批量插入至 MongoDB
		_, err := task.mgoColl.InsertMany(context.TODO(), datas)
		act.FinishTime = time.Now()

		// 如果写入出错，则等待3秒后立即尝试重写
		if err != nil {
			act.Err = err
			rlog.Errorf(`异步批量插入 %v.%v 发生错误 %v`, task.mgoColl.Database().Name(), task.mgoColl.Name(), err)
			task.pendingDatas = datas
			time.Sleep(time.Second * 3)
			continue
		}
	}
}
